/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */
#include "Channel.h"
#include "Hardware.h"
#include <FMedia.h>
#include <math/utl.h>
#include <noise/Manager.h>

using namespace Osp::Base;
using namespace Osp::Media;

namespace liba {
namespace noise {
namespace hard {
namespace samsung {

void onChannelFinished() {
}

static double calculate_attenuation(double distance, double min_distance, double max_distance)
{
	distance = math::clamp( distance, min_distance, max_distance );
	return pow( 2.0, -distance/min_distance);
}

/*static const int frequency_2d = 22050;
static const int frequency_3d = 22050;
static const int frequency_surround = 22050;
static const double minimum_update_part = 0.25;*/

ChannelStaticRep::ChannelStaticRep(const ChannelStaticRepName & name)
	: current_max_volume(0)
	, mem(0)
{
	source = Manager::create_datasource( name.name );
	if( !source )
		throw GluckBadResource( __FUNCTION__, "Could not create DataSource for static channel", name.name );

	memset(&wfx, 0, sizeof(WAV));

	if( name.channel_type == hard::Hardware::channel_type_2d )
	{
		wfx.nChannels = 2;
		wfx.nSamplesPerSec = source->get_frequency();
		wfx.wBitsPerSample = 16;
		wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels;
		wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
	}
	if( name.channel_type == hard::Hardware::channel_type_3d )
	{
		wfx.nChannels = 1;
		wfx.nSamplesPerSec = source->get_frequency();
		wfx.wBitsPerSample = 16;
		wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels;
		wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
	}
	if( name.channel_type == hard::Hardware::channel_type_surround )
	{
		THROW(Gluck( __FUNCTION__, "No surround channels here" ));
	}

	int size = wfx.nBlockAlign * source->get_length();

	result bada_r = buffer.Construct(size);
	if (E_SUCCESS != bada_r)
		throw Gluck(__FUNCTION__, "Couldn't construct audio buffer");

	mem = new char[ size ];
	char* dst_ptr = mem;
	int required_samples = size / wfx.nBlockAlign;

	while( required_samples )
	{
		int decoded_samples = source->decode( source->get_length() );
		if( decoded_samples == 0 )
		{
			logs::log() << logs::error << "SoundSource failed to decode promissed number of samples: " << name.name << "\n";
			break; // very bad
		}
		required_samples -= decoded_samples;

		if( name.channel_type == hard::Hardware::channel_type_2d )
		{
			source->get_data( DataSource::STEREO_LEFT, dst_ptr, wfx.nBlockAlign, 16 );
			source->get_data( DataSource::STEREO_RIGHT, dst_ptr + 2, wfx.nBlockAlign, 16 );
		}
		if( name.channel_type == hard::Hardware::channel_type_3d )
		{
			source->get_data( DataSource::MONO, dst_ptr, wfx.nBlockAlign, 16 );
		}
		if( name.channel_type == hard::Hardware::channel_type_surround )
		{
			THROW(Gluck( __FUNCTION__, "No surround channels here" ));
		}
		dst_ptr += decoded_samples * wfx.nBlockAlign;
	}

	buffer.SetArray(reinterpret_cast<byte*>(mem), 0, size);
	delete[] mem;
	mem = 0;
	source = 0;
}

ChannelStaticRep::~ChannelStaticRep()
{
	if (mem != 0)
		delete[] mem;
	mem = 0;
	//TODO: release sound here
}

Channel::Channel():
	exact_volume( 1 ),
	exact_pan( 0 ),
	min_distance( 1 ),
	max_distance( 1E6 ),
	loop( true )
{}

Channel::~Channel()
{}

bool Channel::change_attribute(const Atom & name, const math::Vector3<float> & value)
{
	if( name == position_name )
	{
		exact_position = value;
		return true;
	}
	if( name == speed_name )
	{
		exact_speed = value;
		return true;
	}
	return hard::Channel::change_attribute(name, value);
}

bool Channel::change_attribute(const Atom & name, double value)
{
	if( name == volume_name )
	{
		exact_volume = math::clamp<double>( value, 0, 1);
		return true;
	}
	if( name == loop_name )
	{
		loop = (value != 0);
		return true;
	}
	if( name == pan_name )
	{
		exact_pan = value;
		return true;
	}
	if( name == minimum_distance_name )
	{
		min_distance = value;
		return true;
	}
	if( name == maximum_distance_name )
	{
		max_distance = value;
		return true;
	}
	return hard::Channel::change_attribute(name, value);
}

bool Channel::get_attribute(const Atom & name, math::Vector3<float> * value)const
{
	return hard::Channel::get_attribute(name, value);
}

bool Channel::get_attribute(const Atom & name, double * value)const
{
	if( name == volume_name )
	{
		*value = exact_volume;
		return true;
	}
	return hard::Channel::get_attribute(name, value);
}

ChannelStatic::ChannelStatic(Hardware * ha, const Atom & channel_type, const filesystem::String & name)
	: finished( true )
	, approx_vol( 0 )
	, position( 0 )
	, channel_type( channel_type )
	, in_existance_flag(false)
{
	load_rep(ChannelStaticRepName(ha, channel_type, name));
	ha->add_static_channel(this);
}

ChannelStatic::~ChannelStatic()
{
	if( !get_rep() )
		return;

	get_rep()->current_max_volume = 0;
	in_existance_flag = false;
	stop();
	get_rep_name().hardware->remove_static_channel(this);
}


void ChannelStatic::life_cycle(double delta_time, const math::Vector3<float> & listener_position)
{
	if( !get_rep() )
		return;
	approx_vol = calculate_attenuation( exact_position.length(listener_position), min_distance, max_distance );
	if( in_existance_flag )
	{
		update_volume();
	}
}

void ChannelStatic::prepare_audio_out()
{
	if (audioOut)
		audioOut.release();

	audioOut = AudioOutPool::GetInstance()->Request(get_rep()->wfx.nChannels == 2);
	if (audioOut)
	{
		audioOut.Subscribe(this);
		update_volume();
	}
}

void ChannelStatic::write_first_chunk()
{
	int maxSize = audioOut->GetMaxBufferSize();
	int oldLimit = get_rep()->buffer.GetLimit();
	buffer_position = math::min(oldLimit, maxSize);
	get_rep()->buffer.SetPosition(0);
	get_rep()->buffer.SetLimit(buffer_position);
	AutoPtr<ByteBuffer> tempBuffer = get_rep()->buffer.SliceN();
	get_rep()->buffer.SetLimit(oldLimit);
	tempBuffer->SetLimit(buffer_position);

	result bada_r = audioOut->WriteBuffer(*tempBuffer);

	if (E_SUCCESS != bada_r)
		AppLog("Couldn't write audio data to audio out");
}

void ChannelStatic::AudioOutEndOfBuffer()
{
	// If there is a portion of buffer that isn't played yet
	if (get_rep()->buffer.GetLimit() > buffer_position)
	{
		// Fill buffer once again.
		int maxSize = audioOut->GetMaxBufferSize();
		int remaining_portion = get_rep()->buffer.GetLimit() - buffer_position;
		int portion = math::min(remaining_portion, maxSize);

		int oldLimit = get_rep()->buffer.GetLimit();
		get_rep()->buffer.SetPosition(buffer_position);
		get_rep()->buffer.SetLimit(buffer_position + portion);
		AutoPtr<ByteBuffer> tempBuffer = get_rep()->buffer.SliceN();
		get_rep()->buffer.SetLimit(oldLimit);
		tempBuffer->SetLimit(portion);

		audioOut->WriteBuffer(*tempBuffer);
		buffer_position += portion;
		return;
	}
	if (loop)
	{
		// If looping, restart buffer.
		write_first_chunk();
	}
	else
	{
		// If not looping, release audio output.
		release_audio_out();
	}
}

void ChannelStatic::release_audio_out()
{
	if (audioOut)
	{
		audioOut->Stop();
		audioOut.Unsubscribe();
		audioOut.release();
	}
}

void ChannelStatic::update_volume()
{
	if (audioOut)
	{
		double doubleVolume;

		if (get_rep_name().channel_type == hard::Hardware::channel_type_3d)
			doubleVolume = approx_vol;
		else
			doubleVolume = exact_volume;

		double global_volume;
		if (noise::Manager::get_hardware()->get_attribute(noise::hard::Hardware::global_volume_name, &global_volume))
			doubleVolume *= global_volume;

		int volume = static_cast<int>(99.0 * doubleVolume);
		audioOut->SetVolume(volume);
	}
}

void ChannelStatic::come_to_existance()
{
	if( !get_rep() )
		return;

	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		//we have other sound that is louder
		if(get_rep()->current_max_volume >= approx_vol)
			return;
	}

	get_rep()->current_max_volume = approx_vol;

	// set sound volume here (exact_volume)
	update_volume();
	// Looping is handled in on_buffer_finished

	prepare_audio_out();
	// play sound
	if (audioOut)
	{
		write_first_chunk();
		result bada_r = audioOut->Start();
		if (E_SUCCESS != bada_r)
			AppLogException("Failed to start sound playback");
	}

	in_existance_flag = true;
}

void ChannelStatic::begone()
{
	if(!in_existance_flag)
		return;

	in_existance_flag = false;

	if( !get_rep() )
		return;

	get_rep()->current_max_volume = 0;

	release_audio_out();
}

void ChannelStatic::play(DataSource * source)
{
	if( !get_rep() )
		return;

	stop();
	position = 0;
	finished = false;

	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
//play the channel but do not activate it yet, hardware will activate it based on its volume
		get_rep_name().hardware->play_static_3d_channel(this);
	}
	else
	{
		come_to_existance();
	}
}

void ChannelStatic::stop()
{
	if( !get_rep() )
		return;

	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
//notify hardware that channel have stopped
		get_rep_name().hardware->stop_static_3d_channel(this);
	}

	if( in_existance() )
		begone();

	finished = true;
	position = 0;
}

bool ChannelStatic::is_finished()
{
	if( !get_rep() )
		return true;

	if( get_rep_name().channel_type != hard::Hardware::channel_type_3d )
		life_cycle(0, math::Vector3<float>() );

	return finished;
}

void ChannelStatic::on_hardware_delete()
{
	stop();
	unload_rep();
}

} // namespace samsung
} // namespace hard
} // namespace noise
} // namespace liba
